import { addDec, subDec, mulDec, divDec, max, min, pow, negated } from './maths';
export interface ISpecial extends IFormulaItem {
  type: 'special';
  value: ':' | ',';
}
export interface IValueBoolean extends IFormulaItem {
  type: 'value';
  dataType: 'boolean';
  value: boolean;
}
type TAllTypes = IOperate | IValueNumber | IBrackets | IFunction | IPlusMinus | ISpecial | IValueBoolean | IKeyword;
type IValue = IValueNumber | IValueBoolean;
type SYSTEM_KEYWORD = 'null' | 'undefined';
interface IKeyword extends IFormulaItem {
  type: 'keyword';
  value: SYSTEM_KEYWORD;
}
interface IPlusMinus extends IFormulaItem {
  type: 'plusMinus';
  value: 'plus' | 'minus';
}
interface IBrackets extends IFormulaItem {
  type: 'brackets';
  value: 'left' | 'right';
}
interface IOperate extends IFormulaItem {
  type: 'operate';
  value: OperateSymbol;
}
interface IValueNumber extends IFormulaItem {
  type: 'value';
  dataType: 'number';
  value: number | string;
}
interface IFormulaItem {
  type: string;
}
interface IFunction extends IFormulaItem {
  type: 'function';
  value: string;
}
const BASE_OPERATE: OperateSymbol[] = ['+', '-', '*', '/', '==', '!=', '!', '>', '>=', '<', '<=', '**', '%'];
const BASE_OPERATE_PRIORITY = {
  '+': 50,
  '-': 50,
  '*': 60,
  '/': 60,
  '%': 60,
  '**': 70,
  '!': 80,
  '==': 90,
  '!=': 90,
  '>': 90,
  '<': 90,
  '>=': 90,
  '<=': 90,
};
export class FormulaError {
  type = 'error';
  dataType = 'string';
  protected info!: any;
  constructor(protected msg: string, info?: any) {
    if (info) {
      if (typeof info === 'object') {
        try {
          this.info = JSON.parse(JSON.stringify(info));
        } catch (error) {
          this.info = { originInfo: info, error };
        }
      } else {
        this.info = info;
      }
    }
  }
  toString() {
    return this.msg;
  }
  get value() {
    return this.msg;
  }
}
type OperateSymbol = keyof typeof BASE_OPERATE_PRIORITY;
type ErrorInfo = null | FormulaError;
type ParseFunctionResult = [ErrorInfo, TAllTypes, number];
type ParseFunction = (str: string, startIdx: number, pastParse: TAllTypes[]) => ParseFunctionResult;

const paseOperate: ParseFunction = (str: string, start: number, result: TAllTypes[]) => {
  const pastParse = result[result.length - 1] as IBrackets | IOperate;
  if (
    start === 0 ||
    ((str[start] === '+' || str[start] === '-') && result.length > 0 && pastParse.type === 'brackets' && pastParse.value === 'left') ||
    pastParse.type === 'operate'
  ) {
    const val: IPlusMinus = {
      type: 'plusMinus',
      value: str[start] === '+' ? 'plus' : 'minus',
    };
    return [null, val, start];
  }
  if (
    start < str.length - 1 &&
    ['>', '<', '*'].indexOf(str[start]) !== -1 &&
    (BASE_OPERATE.indexOf(str[start + 1] as OperateSymbol) !== -1 || str[start + 1] === '=') &&
    ['+', '-'].indexOf(str[start + 1]) === -1
  ) {
    const val: IOperate = {
      type: 'operate',
      value: str.substring(start, start + 2) as OperateSymbol,
    };
    return [null, val, start + 1];
  }
  if (str[start] === '!' || str[start] === '=') {
    if (str[start + 1] !== '=') {
      // throw error;
    }
    const val: IOperate = {
      type: 'operate',
      value: str.substring(start, start + 1) as OperateSymbol,
    };
    return [null, val, start + 1];
  }

  const val: IOperate = {
    type: 'operate',
    value: str[start] as OperateSymbol,
  };
  return [null, val, start];
};
const parseNumber: ParseFunction = (str: string, start: number) => {
  let endIdx = str.length - 1;
  for (let i = start; i < str.length; i++) {
    if (!/[0-9]/.test(str[i]) && str[i] && str[i] !== '.') {
      endIdx = i - 1;
      break;
    }
  }
  const _val = str.substring(start, endIdx + 1);
  const val: IValueNumber = {
    type: 'value',
    dataType: 'number',
    value: String(_val),
  };
  return [null, val, endIdx];
};
/**
 *
 * @param str st
 * @param start
 */
const parseFunc: ParseFunction = (str: string, start: number) => {
  let endIdx = str.length;
  for (let i = start; i < str.length; i++) {
    if (!/^[A-Z0-9\$]$/.test(str[i])) {
      endIdx = i;
      break;
    }
  }
  const subStr = str.substring(start, endIdx);
  const val: IFunction = {
    type: 'function',
    value: subStr,
  };
  return [null, val, endIdx - 1];
};
const parseString: ParseFunction = (str: string, start: number) => {
  let endIdx = str.length;
  for (let i = start; i < str.length; i++) {
    if (!/^[a-z\$]$/.test(str[i])) {
      endIdx = i;
      break;
    }
  }
  const subStr = str.substring(start, endIdx) as SYSTEM_KEYWORD;
  const val: IKeyword = {
    type: 'keyword',
    value: subStr,
  };
  return [null, val, endIdx - 1];
};
export function parse(str: string) {
  const result: TAllTypes[] = [];
  for (let i = 0; i < str.length; i++) {
    if (str[i] === ' ') continue;
    if (str[i] === '(') {
      result.push({ type: 'brackets', value: 'left' });
      continue;
    }
    if (str[i] === ')') {
      result.push({ type: 'brackets', value: 'right' });
      continue;
    }
    if (str[i] === ',') {
      result.push({
        type: 'special',
        value: ',',
      });
      continue;
    }
    if (BASE_OPERATE.indexOf(str[i] as OperateSymbol) !== -1) {
      const [err, val, newStartIdx] = paseOperate(str, i, result);
      if (!err) {
        result.push(val);
        i = newStartIdx;
        continue;
      }
    }
    if (/[0-9]/.test(str[i])) {
      const [err, val, newStartIdx] = parseNumber(str, i, result);
      if (!err) {
        result.push(val);
        i = newStartIdx;
        continue;
      }
    }
    if (/[a-z]/.test(str[i])) {
      const [err, val, newStartIdx] = parseString(str, i, result);
      if (!err) {
        result.push(val);
        i = newStartIdx;
        throw new FormulaError('val! error!', { val });
      }
    }
    if (/[A-Z]/.test(str[i])) {
      const [err, val, newStartIdx] = parseFunc(str, i, result);
      if (!err) {
        result.push(val);
        i = newStartIdx;
        continue;
      }
    }
  }
  return result;
}
export const safeWhile = (func: (idx: number) => boolean | undefined, max = 1000) => {
  let idx = 0;
  while (true) {
    const manualBreck = func(idx);
    if (idx++ === max || manualBreck) {
      break;
    }
  }
};
export function rebuildCalculate(calcArr: TAllTypes[]) {
  const result: TAllTypes[] = [];
  const operates: TAllTypes[] = [];
  for (let i = 0; i < calcArr.length; i++) {
    if (calcArr[i].type === 'value') {
      result.push(calcArr[i]);
      if (operates.length > 0) {
        const preOperate = operates[operates.length - 1];
        if (preOperate.type === 'special') {
          if (preOperate.value === ':') {
            result.push(operates.pop() as TAllTypes);
          }
        }
      }
      continue;
    }
    if (calcArr[i].type === 'special' && (calcArr[i] as ISpecial).value === ',') {
      operates.push(calcArr[i]);
      continue;
    }
    if (calcArr[i].type === 'brackets') {
      const currentOperate = calcArr[i] as IBrackets;
      if (currentOperate.value === 'left') {
        operates.push(currentOperate);
        continue;
      }
      safeWhile(() => {
        if (operates.length === 0) {
          throw new FormulaError('#REF!', { errorId: '0147f8c9c585' });
        }
        const tempOpreate = operates.pop() as IBrackets;
        if (tempOpreate.type === 'brackets' && tempOpreate.value === 'left') {
          if (operates.length > 0 && operates[operates.length - 1].type === 'function') {
            result.push(operates.pop() as TAllTypes);
            if (operates.length > 0 && operates[operates.length - 1].type === 'plusMinus') {
              result.push(operates.pop() as TAllTypes);
            }
          }
          return true;
        } else {
          result.push(tempOpreate);
        }
        return false;
      });
      if (operates.length > 0 && operates[operates.length - 1].type === 'plusMinus') {
        result.push(operates.pop() as TAllTypes);
      }
      continue;
    }
    if (calcArr[i].type === 'plusMinus') {
      if (operates.length === 0) {
        operates.push(calcArr[i]);
        continue;
      }
      if (operates[operates.length - 1].type === 'operate') {
        operates.push(calcArr[i]);
        continue;
      }
    }
    if (calcArr[i].type === 'operate') {
      const currentOperate = calcArr[i] as IOperate;
      if (operates.length === 0) {
        operates.push(currentOperate);
        continue;
      }
      const preOperate = operates[operates.length - 1];
      if (preOperate.type === 'plusMinus') {
        result.push(operates.pop() as TAllTypes);
        operates.push(currentOperate as TAllTypes);
        continue;
      }
      if (preOperate.type !== 'operate') {
        operates.push(currentOperate);
        continue;
      }

      const priority = BASE_OPERATE_PRIORITY[currentOperate.value];
      safeWhile(() => {
        if (operates.length === 0 || operates[operates.length - 1].type !== 'operate') {
          operates.push(currentOperate);
          return true;
        }
        const prepPriority = BASE_OPERATE_PRIORITY[preOperate.value];
        if (priority > prepPriority) {
          operates.push(currentOperate);
          return true;
        } else {
          result.push(operates.pop() as TAllTypes);
        }
        return false;
      });
    }
    if (calcArr[i].type === 'function') {
      operates.push(calcArr[i]);
    }
    if (calcArr[i].type === 'special') {
      if ((calcArr[i] as ISpecial).value === ':') {
        safeWhile(() => {
          if (operates.length === 0) {
            return true;
          }
          const preOperate = operates[operates.length - 1];
          if (preOperate.type === 'brackets' || preOperate.type === 'operate') {
            return true;
          }
          result.push(operates.pop() as TAllTypes);
        });
        operates.push(calcArr[i]);
      }
      if ((calcArr[i] as ISpecial).value === ',') {
        safeWhile(() => {
          if (operates.length === 0) {
            return true;
          }
          const preOperate = operates[operates.length - 1];
          if (preOperate.type === 'function' || (preOperate.type === 'special' && preOperate.value === ',')) {
            operates.push(calcArr[i]);
            return true;
          }
          result.push(operates.pop() as TAllTypes);
          return false;
        });
      }
    }
  }
  return result.concat(operates.reverse());
}

const funcs = {
  max: (vals: any[]) => {
    return max(...(<any>vals));
  },
  min: (vals: any[]) => {
    return min(<any>vals);
  },
};
function calc(operate: OperateSymbol | keyof typeof funcs | 'plusMinus' | 'function', vals: TAllTypes[]): TAllTypes {
  for (const item of vals) {
    if (item.type !== 'value' && item.type !== 'special' && item.type !== 'plusMinus' && item.type !== 'function') {
      throw new FormulaError(`expect vals`, { vals: vals });
    }
  }
  if (operate === '!=') {
    return { type: 'value', dataType: 'boolean', value: String((vals[0] as IValue).value) != String((vals[1] as IValue).value) };
  }
  if (operate === '==') {
    return { type: 'value', dataType: 'boolean', value: String((vals[0] as IValue).value) == String((vals[1] as IValue).value) };
  }
  if (operate === 'plusMinus') {
    const _val = negated(vals[0].value as string);
    return { type: 'value', dataType: 'number', value: _val };
  }
  const val1 = (vals as IValueNumber[])[0].value;
  const val2 = (vals as IValueNumber[])[1].value;
  if (operate === '+') {
    return { type: 'value', value: addDec(val1, val2), dataType: 'number' };
  }
  if (operate === '-') {
    return { type: 'value', dataType: 'number', value: subDec(val1, val2) };
  }
  if (operate === '*') {
    return { type: 'value', dataType: 'number', value: mulDec(val1, val2) };
  }
  if (operate === '/') {
    if (val2 === 0 || Number(val2) === 0) {
      throw new FormulaError('#DIV/0!', { vals: vals });
    }
    return { type: 'value', dataType: 'number', value: divDec(val1, val2) };
  }
  if (operate === '**') {
    return { type: 'value', dataType: 'number', value: pow(val1, val2) };
  }
  if (operate === '%') {
    throw new FormulaError(`not support, operate %`, { vals: vals });
    // return { type: 'value', dataType: 'number', value: new Decimal(val1 % val2).toString() };
  }
  if (operate === '>') {
    return { type: 'value', dataType: 'boolean', value: vals[0] > vals[1] };
  }
  if (operate === '>=') {
    if (vals[0] === undefined || vals[1] === undefined) {
      throw new FormulaError('#N/A', { operate: '!=', info: vals });
    }
    return { type: 'value', dataType: 'boolean', value: vals[0] >= vals[1] };
  }
  if (operate === '<') {
    if (vals[0] === undefined || vals[1] === undefined) {
      throw new FormulaError('#N/A', { operate: '!=', info: vals });
    }
    return { type: 'value', dataType: 'boolean', value: vals[0] < vals[1] };
  }
  if (operate === '<=') {
    if (vals[0] === undefined || vals[1] === undefined) {
      throw new FormulaError('#N/A', { operate: '!=', info: vals });
    }
    return { type: 'value', dataType: 'boolean', value: vals[0] <= vals[1] };
  }
  if (Object.keys(funcs).indexOf(operate.toLocaleLowerCase()) !== -1) {
    const funcName = operate.toLocaleLowerCase() as keyof typeof funcs;
    for (const val of vals) {
      if (val.type !== 'value' && val.type !== 'special') {
        throw new FormulaError('#N/A', { operate: 'max', info: vals });
      }
    }
    const _vals = vals.filter((item) => item.type === 'value').map((item) => item.value);
    return {
      type: 'value',
      dataType: 'number',
      value: funcs[funcName](_vals),
    };
  }
  throw new FormulaError('#UNKOWN!', { msg: 'unkown operate', operate: operate, vals: vals });
}

function _calculateResult(vals: TAllTypes[], processCallback: (currentVals: TAllTypes[], calc: any, result: any) => void = () => {}): any {
  processCallback(vals, [], []);
  let _calc: any = [];
  for (let i = 0; i < vals.length; i++) {
    if (vals[i].type === 'plusMinus') {
      if (vals[i].value === 'minus') {
        const res = calc('plusMinus', [vals[i - 1]]);
        _calc = vals.splice(i - 1, 2, res);
        processCallback(vals, _calc, res);
        i = i - 1;
      } else {
        _calc = vals.splice(i, 1);
        processCallback(vals, _calc, []);
        i--;
      }
      continue;
    }
    if (vals[i].type === 'operate') {
      const res = calc(vals[i].value as any, [vals[i - 2], vals[i - 1]]);
      _calc = vals.splice(i - 2, 3, res);
      i = i - 3;
      processCallback(vals, _calc, res);
      continue;
    }
    if (vals[i].type === 'function') {
      let j = i - 1;
      for (; j >= 0; j--) {
        if (vals[j].type !== 'special') {
          break;
        }
      }
      const start = i - ((i - j - 1) * 2 + 1);
      const count = i - start + 1;
      const _vals = vals.slice(start, i);
      const res = calc(vals[i].value as 'function', _vals);
      _calc = vals.splice(start, count, res);
      i = start - 1;
      processCallback(vals, _calc, res);
      continue;
    }
  }
  return vals;
}
type PrecisionType = 'floor' | 'ceil' | 'round';
/**
 * 
 * @param val 
 * @param precision 
 * @param precisionType default {floor}
 * @returns 
 */
export function handlePrecision(val: number | string, precision: number | string | undefined | null, precisionType: PrecisionType = 'floor') {
  const _val = Number(val);
  let _precision = Number(precision);
  if (isNaN(_val)) {
    throw new Error(`params unexpected! number: ${val}, precision: ${precision}`);
  }
  if (!isNaN(_precision) && _precision < 0) {
    throw new Error(`precision must bigger than 0, but received ${precision}`);
  }
  const _valStr = String(val);
  const dotIdx = _valStr.indexOf('.');
  let start = _valStr.substring(0, dotIdx === -1 ? _valStr.length : dotIdx);
  let end = dotIdx ===  -1 ? ''  : _valStr.substring(dotIdx + 1);
  _precision = isNaN(_precision) ? end.length : _precision;
  // console.table({dotIdx, start, end});
  let result = '0';
  if (_precision === 0) {
    result = start;
  } else {
    if (end === '') {
      result = start;
    } else {

      if(_precision >= end.length) {
        result = start + '.' + end;
      } else {
        let endNum = Number(end.substring(0, _precision));
        let judgeNum = Number(end[_precision]);
        let otherNum = Number(end.substring(_precision));
        // console.table({endNum, judgeNum, otherNum})
        if(precisionType === 'floor') {
          result =  start + '.' + String(endNum);
        }
        if(precisionType === 'ceil') {
          let newEnd = otherNum > 0 ? endNum + 1 : endNum;
          if(String(newEnd).length > String(endNum).length) {
            start = String(Number(start) + 1);
            newEnd = Number(String(newEnd).substring(1));
          }
          result = start + '.' + newEnd;
        }
        if(precisionType === 'round') {
          let newEnd = judgeNum >= 5 ? endNum + 1 : endNum;
          if(String(newEnd).length > String(endNum).length) {
            start = String(Number(start) + 1);
            newEnd = Number(String(newEnd).substring(1));
          }
          result = start + '.' + newEnd;
        }
      }
    }
  }
  if (Number(result) === 0) return '0';
  return Number(result).toString();
}
type CalculateErrorHandleOption = ((err: any, info: { parse: any; rebuild: any; process: any }) => any) | string | number;
type CalculateOption = {
  errHandle?: CalculateErrorHandleOption;
  precision?: number;
  precisionType?: PrecisionType
};
/**
 * @description 混合运算
 * @param str 字符串
 * @param errHandle 错误处理，默认不处理则抛出错误
 * @example
 * ```typescript
 * import {calculate} from './calutate';
 * let result = calculate('1 + 2*3+MAX(4,5)');
 * console.log(result); // 11
 * ```
 * @returns
 */
export function calculate(str: string, option?: CalculateErrorHandleOption | CalculateOption, hasProcess = false): string {
  let parseResult: any[] = [];
  let rebuildResult: any[] = [];
  let logRebuild: any[] = [];
  const process: any[] = [];
  const _option: CalculateOption = (() => {
    if (typeof option === 'object') {
      return option;
    }
    return {
      errHandle: option,
      precision: undefined,
    };
  })();
  try {
    parseResult = parse(str);
    rebuildResult = rebuildCalculate(parseResult);
    if (hasProcess) {
      logRebuild = JSON.parse(JSON.stringify(rebuildResult));
    }
    const res = _calculateResult(rebuildResult, (_vals, calc, result) => {
      if (hasProcess === false) return;
      process.push({
        finally: JSON.parse(JSON.stringify(_vals)),
        calc: JSON.parse(JSON.stringify(calc)),
        result: JSON.parse(JSON.stringify(result)),
      });
    });
    if (res.length !== 1 || !res[0] || res[0].type !== 'value') {
      throw new FormulaError('capulet error!', { val: res });
    }
    if (hasProcess && typeof _option.errHandle === 'function') {
      _option.errHandle(null, { parse: parseResult, rebuild: logRebuild, process: process });
    }
    return handlePrecision(res[0].value, _option.precision, _option.precisionType);
  } catch (error) {
    if (_option.errHandle !== undefined) {
      return typeof _option.errHandle === 'function'
        ? _option.errHandle(error, { parse: parseResult, rebuild: logRebuild, process: process })
        : _option.errHandle;
    } else {
      console.error(error);
      throw new Error('计算错误！');
    }
  }
}
